using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Threading;
using OpenMetaverse;

namespace OpenFuse.Backend
{
    public class OarFile
    {
        enum ProfileShape : byte
        {
            Circle = 0,
            Square = 1,
            IsometricTriangle = 2,
            EquilateralTriangle = 3,
            RightTriangle = 4,
            HalfCircle = 5
        }

        TarArchiveWriter _archive = new TarArchiveWriter();
		
		public OarFile()
		{
            const string ARCHIVE_XML = "<?xml version=\"1.0\" encoding=\"utf-16\"?>\n<archive major_version=\"0\" minor_version=\"2\" />";

			// Create the archive.xml file
            _archive.AddSignatureFile(ArchiveConstants.CONTROL_FILE_PATH, ARCHIVE_XML);
		}

		public byte[] PackageArchive()
        {
			MemoryStream memBlock = new MemoryStream();
            _archive.WriteTar(new GZipStream(memBlock, CompressionMode.Compress));
			return memBlock.ToArray();
        }

        public void SaveLinkset(SlSession session, LinkSet linkset, string path, UUID overrideID)
        {
			string filename;
			UUID objectID;
			if(overrideID == UUID.Zero)
				objectID = linkset.Root.ID;
			else
				objectID = overrideID;
			

			filename = objectID.ToString() + ArchiveConstants.ASSET_TYPE_TO_EXTENSION[(sbyte)AssetType.Object];
			
			try
            {
				if((linkset.Root.Flags & PrimFlags.InventoryEmpty) == PrimFlags.InventoryEmpty)
					session.Logger.DebugFormat("Prim '{0}' has no task inventory", linkset.Root.Properties.Name);
				else
					session.Logger.DebugFormat("Prim '{0}' does have a task inventory", linkset.Root.Properties.Name);
				
				using( MemoryStream memory = new MemoryStream())
				{
	                using (StreamWriter stream = new StreamWriter(memory))
	                {
	                    XmlTextWriter writer = new XmlTextWriter(stream);
	                    SOGToXml2(writer, linkset, objectID);
	                    writer.Flush();
	                }
	            	_archive.AddAssetFile(path + filename, memory.ToArray());
				}
            }
            catch (Exception ex)
            {
                Logger.Log("Failed saving linkset: " + ex.ToString(), Helpers.LogLevel.Error);
            }
        }

		#region SOG to XML code
        static void SOGToXml2(XmlTextWriter writer, LinkSet linkset, UUID overriddenUUID)
        {
            writer.WriteStartElement(String.Empty, "SceneObjectGroup", String.Empty);
			
			writer.WriteStartElement("RootPart");
            SOPToXml(writer, linkset.Root, null, overriddenUUID);
			writer.WriteEndElement();
			
            writer.WriteStartElement(String.Empty, "OtherParts", String.Empty);

            foreach(BackendPrimitive child in linkset.TrueChildren)
                SOPToXml(writer, child, linkset.Root, child.ID);

            writer.WriteEndElement();
            writer.WriteEndElement();
        }

        static void SOPToXml(XmlTextWriter writer, BackendPrimitive prim, BackendPrimitive parent, UUID overriddenUUID)
        {
            writer.WriteStartElement("SceneObjectPart");
            writer.WriteAttributeString("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
            writer.WriteAttributeString("xmlns:xsd", "http://www.w3.org/2001/XMLSchema");

            WriteUUID(writer, "CreatorID", prim.Properties.CreatorID);
            WriteUUID(writer, "FolderID", prim.Properties.FolderID);
            writer.WriteElementString("InventorySerial", prim.Properties.InventorySerial.ToString());
            writer.WriteStartElement("TaskInventory");
			
			if(prim.Inventory != null)
			{
				foreach(InventoryItem i in prim.Inventory)
				{
					writer.WriteStartElement("TaskInventoryItem");
					WriteUUID(writer, "AssetID", i.AssetUUID);
					writer.WriteElementString("BasePermissions", ((int)i.Permissions.BaseMask).ToString());
					writer.WriteElementString("CreationDate", ((int)Utils.DateTimeToUnixTime(i.CreationDate)).ToString());
					WriteUUID(writer, "CreatorID", i.CreatorID);
					writer.WriteElementString("Description", i.Description);
					writer.WriteElementString("EveryonePermissions", ((int)i.Permissions.EveryoneMask).ToString());
					writer.WriteElementString("Flags", i.Flags.ToString());
					WriteUUID(writer, "GroupID", i.GroupID);
					writer.WriteElementString("GroupPermisssions", ((int)i.Permissions.GroupMask).ToString());
					writer.WriteElementString("InvType", ((int)i.InventoryType).ToString());
					WriteUUID(writer, "ItemID", i.UUID);
					WriteUUID(writer, "LastOwnerID", i.OwnerID);
					writer.WriteElementString("Name", i.Name);
					writer.WriteElementString("NextPermissions", ((int)i.Permissions.NextOwnerMask).ToString());
					WriteUUID(writer, "OwnerID", i.OwnerID);
					writer.WriteElementString("CurrentPermissions", ((int)i.Permissions.OwnerMask).ToString());
					WriteUUID(writer, "ParentID", i.ParentUUID);
					WriteUUID(writer, "ParentPartID", i.ParentUUID);
					WriteUUID(writer, "PermsGranter", UUID.Zero);
					writer.WriteElementString("PermsMask", "0");
					writer.WriteElementString("Type", "0");
					writer.WriteEndElement();
				}
			}
			
			writer.WriteEndElement();
            writer.WriteElementString("ObjectFlags", ((int)prim.Flags).ToString());
            WriteUUID(writer, "UUID", overriddenUUID);
            writer.WriteElementString("LocalId", prim.LocalID.ToString());
            writer.WriteElementString("Name", prim.Properties.Name);
            writer.WriteElementString("Material", ((int)prim.PrimData.Material).ToString());
            writer.WriteElementString("RegionHandle", prim.RegionHandle.ToString());
            writer.WriteElementString("ScriptAccessPin", "0");

            Vector3 groupPosition;
            if (parent == null)
                groupPosition = prim.Position;
            else
                groupPosition = parent.Position;

            WriteVector(writer, "GroupPosition", groupPosition);
            WriteVector(writer, "OffsetPosition", groupPosition - prim.Position);
            WriteQuaternion(writer, "RotationOffset", prim.Rotation);
            WriteVector(writer, "Velocity", Vector3.Zero);
            WriteVector(writer, "RotationalVelocity", Vector3.Zero);
            WriteVector(writer, "AngularVelocity", prim.AngularVelocity);
            WriteVector(writer, "Acceleration", Vector3.Zero);
            writer.WriteElementString("Description", prim.Properties.Description);
            writer.WriteStartElement("Color");
            writer.WriteElementString("R", prim.TextColor.R.ToString());
            writer.WriteElementString("G", prim.TextColor.G.ToString());
            writer.WriteElementString("B", prim.TextColor.B.ToString());
            writer.WriteElementString("A", prim.TextColor.G.ToString());
            writer.WriteEndElement();
            writer.WriteElementString("Text", prim.Text);
            writer.WriteElementString("SitName", prim.Properties.SitName);
            writer.WriteElementString("TouchName", prim.Properties.TouchName);

            uint linknum = 0;
            //if (parent != null)
            //    linknum = prim.LocalID - parent.LocalID;

            writer.WriteElementString("LinkNum", linknum.ToString());
            writer.WriteElementString("ClickAction", ((int)prim.ClickAction).ToString());
            writer.WriteStartElement("Shape");

            writer.WriteElementString("PathBegin", Primitive.PackBeginCut(prim.PrimData.PathBegin).ToString());
            writer.WriteElementString("PathCurve", ((byte)prim.PrimData.PathCurve).ToString());
            writer.WriteElementString("PathEnd", Primitive.PackEndCut(prim.PrimData.PathEnd).ToString());
            writer.WriteElementString("PathRadiusOffset", Primitive.PackPathTwist(prim.PrimData.PathRadiusOffset).ToString());
            writer.WriteElementString("PathRevolutions", Primitive.PackPathRevolutions(prim.PrimData.PathRevolutions).ToString());
            writer.WriteElementString("PathScaleX", Primitive.PackPathScale(prim.PrimData.PathScaleX).ToString());
            writer.WriteElementString("PathScaleY", Primitive.PackPathScale(prim.PrimData.PathScaleY).ToString());
            writer.WriteElementString("PathShearX", ((sbyte)Primitive.PackPathShear(prim.PrimData.PathShearX)).ToString());
            writer.WriteElementString("PathShearY", ((sbyte)Primitive.PackPathShear(prim.PrimData.PathShearY)).ToString());
            writer.WriteElementString("PathSkew", Primitive.PackPathTwist(prim.PrimData.PathSkew).ToString());
            writer.WriteElementString("PathTaperX", Primitive.PackPathTaper(prim.PrimData.PathTaperX).ToString());
            writer.WriteElementString("PathTaperY", Primitive.PackPathTaper(prim.PrimData.PathTaperY).ToString());
            writer.WriteElementString("PathTwist", Primitive.PackPathTwist(prim.PrimData.PathTwist).ToString());
            writer.WriteElementString("PathTwistBegin", Primitive.PackPathTwist(prim.PrimData.PathTwistBegin).ToString());
            writer.WriteElementString("PCode", ((byte)prim.PrimData.PCode).ToString());
            writer.WriteElementString("ProfileBegin", Primitive.PackBeginCut(prim.PrimData.ProfileBegin).ToString());
            writer.WriteElementString("ProfileEnd", Primitive.PackEndCut(prim.PrimData.ProfileEnd).ToString());
            writer.WriteElementString("ProfileHollow", Primitive.PackProfileHollow(prim.PrimData.ProfileHollow).ToString());
            WriteVector(writer, "Scale", prim.Scale);
            writer.WriteElementString("State", prim.PrimData.State.ToString());

            ProfileShape shape = (ProfileShape)prim.PrimData.ProfileCurve;
            writer.WriteElementString("ProfileShape", shape.ToString());
            writer.WriteElementString("HollowShape", prim.PrimData.ProfileHole.ToString());
            writer.WriteElementString("ProfileCurve", prim.PrimData.profileCurve.ToString());

            writer.WriteStartElement("TextureEntry");

            byte[] te;
            if (prim.Textures != null)
                te = prim.Textures.GetBytes();
            else
                te = new byte[0];

            writer.WriteBase64(te, 0, te.Length);
            writer.WriteEndElement();

            // FIXME: ExtraParams
            writer.WriteStartElement("ExtraParams"); writer.WriteEndElement();

            writer.WriteEndElement();

            WriteVector(writer, "Scale", prim.Scale);
            writer.WriteElementString("UpdateFlag", "0");
            WriteVector(writer, "SitTargetOrientation", Vector3.UnitZ);
            WriteVector(writer, "SitTargetPosition", Vector3.Zero);
            WriteVector(writer, "SitTargetPositionLL", Vector3.Zero);
            WriteQuaternion(writer, "SitTargetOrientationLL", new Quaternion(0f, 0f, 1f, 0f));
            writer.WriteElementString("ParentID", prim.ParentID.ToString());
            writer.WriteElementString("CreationDate", ((int)Utils.DateTimeToUnixTime(prim.Properties.CreationDate)).ToString());
            writer.WriteElementString("Category", ((int)prim.Properties.Category).ToString());
            writer.WriteElementString("SalePrice", prim.Properties.SalePrice.ToString(Utils.EnUsCulture));
            writer.WriteElementString("ObjectSaleType", ((int)prim.Properties.SaleType).ToString());
            writer.WriteElementString("OwnershipCost", prim.Properties.OwnershipCost.ToString());
            WriteUUID(writer, "GroupID", prim.GroupID);
            WriteUUID(writer, "OwnerID", prim.OwnerID);
            WriteUUID(writer, "LastOwnerID", prim.Properties.LastOwnerID);

			// TODO fixup permissions
			writer.WriteElementString("BaseMask", ((uint)PermissionMask.All).ToString());
            writer.WriteElementString("OwnerMask", ((uint)PermissionMask.All).ToString());
            writer.WriteElementString("GroupMask", ((uint)PermissionMask.All).ToString());
            writer.WriteElementString("EveryoneMask", ((uint)PermissionMask.All).ToString());
            writer.WriteElementString("NextOwnerMask", ((uint)PermissionMask.All).ToString());
            writer.WriteElementString("Flags", "None");
            WriteUUID(writer, "SitTargetAvatar", UUID.Zero);

            writer.WriteEndElement();
        }

        static void WriteUUID(XmlTextWriter writer, string name, UUID id)
        {
            writer.WriteStartElement(name);
            writer.WriteElementString("UUID", id.ToString());
            writer.WriteEndElement();
        }

        static void WriteVector(XmlTextWriter writer, string name, Vector3 vec)
        {
            writer.WriteStartElement(name);
            writer.WriteElementString("X", vec.X.ToString(Utils.EnUsCulture));
            writer.WriteElementString("Y", vec.Y.ToString(Utils.EnUsCulture));
            writer.WriteElementString("Z", vec.Z.ToString(Utils.EnUsCulture));
            writer.WriteEndElement();
        }

        static void WriteQuaternion(XmlTextWriter writer, string name, Quaternion quat)
        {
            writer.WriteStartElement(name);
            writer.WriteElementString("X", quat.X.ToString(Utils.EnUsCulture));
            writer.WriteElementString("Y", quat.Y.ToString(Utils.EnUsCulture));
            writer.WriteElementString("Z", quat.Z.ToString(Utils.EnUsCulture));
            writer.WriteElementString("W", quat.W.ToString(Utils.EnUsCulture));
            writer.WriteEndElement();
        }
		#endregion SOG to XML code

		#region Save textures
		public void DownloadAndSaveLinksetTextures(SlSession session, LinkSet linkset)
		{
			foreach(Primitive p in linkset.Children)
				DownloadAndSavePrimitiveTextures(session, p);

		}

		private void DownloadAndSaveTexture(SlSession session, UUID textureID)
		{
			try
	        {
				// if the texture is protected (or wellknown) then ignore this request
				if(ProtectedAssets.IsAssetProtected(textureID))
				{
					session.Logger.Info("skipping wellknown texture: " + textureID);
					return;
				}
				
				session.Logger.DebugFormat("downloading texture: {0}", textureID.Guid);
				byte[] textureData = Utilities.DownloadTexture(session, textureID);
				if(textureData != null) 
				{
		            string filename = textureID + ArchiveConstants.ASSET_TYPE_TO_EXTENSION[(sbyte)AssetType.Texture];
	
					_archive.AddAssetFile(ArchiveConstants.ASSETS_PATH + filename, textureData);
				} else {
					session.Logger.ErrorFormat("Failed to download texture: {0}", textureID);
				}
	        }
	        catch (Exception ex)
	        {
				session.Logger.ErrorFormat("Failed to download texture: {0}\n{1}", textureID, ex);
	        }
		}
		
		private void DownloadAndSavePrimitiveTextures(SlSession session, Primitive prim)
		{
			
			if(prim.Sculpt != null && prim.Sculpt.SculptTexture != UUID.Zero)
				DownloadAndSaveTexture(session, prim.Sculpt.SculptTexture);
						
			if(prim.Textures.DefaultTexture != null && prim.Textures.DefaultTexture.TextureID != UUID.Zero)
				DownloadAndSaveTexture(session, prim.Textures.DefaultTexture.TextureID);
			
			foreach(OpenMetaverse.Primitive.TextureEntryFace t in prim.Textures.FaceTextures)
				if(t != null && t.TextureID != UUID.Zero)
					DownloadAndSaveTexture(session, t.TextureID);
		}
		#endregion Save textures

		#region Save Task inventory
		public bool DownloadAndSaveTaskInventory(SlSession session, LinkSet linkset)
		{
			bool taskInventoryItemIsProtected = false;
			foreach(BackendPrimitive be in linkset.Children)
			{
				// download each item in the task inventory
				if(be.Inventory != null)
				{
					foreach(InventoryItem taskItem in be.Inventory)
					{
						// if we cannot copy the item, we shouldn't delete when de-rez'ing
						if((taskItem.Permissions.OwnerMask & PermissionMask.Copy) == 0)
							taskInventoryItemIsProtected = true;

						if(taskItem.AssetType == AssetType.Object) {
							session.Logger.DebugFormat("Downloading object task inventory item '{0}'", taskItem.Name);
	
							UUID newInventoryItemID = Utilities.MoveItemFromTaskInventoryToAgentFolder(session, be.LocalID, taskItem.UUID, session.WorkFolderID);
							if(newInventoryItemID == UUID.Zero)
							{
								session.Logger.ErrorFormat("Cannot download '{0}' from inventory of '{1}'", taskItem.Name, be.Properties.Name);
								break;
							}
		
							// then we rez the item from our inventory
							InventoryItem newInventoryItem = (InventoryItem)session.GridClient.Inventory.FetchItem(newInventoryItemID, session.AvatarId, 4000);
							session.Logger.DebugFormat("Rezed item={0}", newInventoryItem.Name);
							Primitive rezedPrimitive = Utilities.RezInventoryItem(session, newInventoryItem);
							if(rezedPrimitive != null)
							{
								// fill the child linkset with all prims in to object group
								LinkSet childLinkset = LinkSet.AutoPopulate(session, new BackendPrimitive(rezedPrimitive));
								
								// get task inventories for all prims in the object group
								childLinkset.DownloadTaskInventories(session);
								
								session.Logger.DebugFormat("------ SaveSubLinkset '{0}' with AssetUUID={1}", childLinkset.Root.Properties.Name, taskItem.AssetUUID);
								this.SaveLinkset(session, childLinkset, ArchiveConstants.ASSETS_PATH, taskItem.AssetUUID);
								this.DownloadAndSaveLinksetTextures(session, childLinkset);
								taskInventoryItemIsProtected |= this.DownloadAndSaveTaskInventory(session, childLinkset);
								
								if(!taskInventoryItemIsProtected)
								{
									session.Logger.DebugFormat("Sub-Object '{0}' is not protected", childLinkset.Root.Properties.Name);
									UUID bogusItemID = Utilities.DeRezObject(session, rezedPrimitive, DeRezDestination.TrashFolder, UUID.Zero);
									Utilities.HardDeleteItem(session, bogusItemID);
									Utilities.HardDeleteItem(session, newInventoryItemID);
								}
								else
								{
									session.Logger.DebugFormat("Sub-Object '{0}' is protected", childLinkset.Root.Properties.Name);
									UUID itemInWorkfolderID = Utilities.DeRezObject(session, rezedPrimitive, DeRezDestination.AgentInventoryTake, session.WorkFolderID);
									if(itemInWorkfolderID != UUID.Zero) {
										InventoryItem newProtectedItem = (InventoryItem)session.GridClient.Inventory.FetchItem(itemInWorkfolderID, session.AvatarId, 4000);
										if(newProtectedItem != null)
											session.GridClient.Inventory.UpdateTaskInventory(be.LocalID, newProtectedItem);
										else
											session.Logger.ErrorFormat("failed to get the proected item '{0}', recently stored in the workfolder", rezedPrimitive.Properties.Name);
									} else
										session.Logger.ErrorFormat("Failed to return '{0}' back into '{1}'", rezedPrimitive.Properties.Name, be.Properties.Name);
								}
							}
						} else if(taskItem.AssetType == AssetType.Gesture) {
							session.Logger.InfoFormat("there is currently no support of gestures '{0}' is ignored", taskItem.Name);
						} else {

							if(taskItem.AssetType == AssetType.LSLText && (taskItem.Permissions.OwnerMask & PermissionMask.Modify) == 0)
							{
								session.Logger.InfoFormat("Task inventory contains protected script, ignoring '{0}'", taskItem.Name);
								return taskInventoryItemIsProtected;
							}
							
							Cache.Asset InventoryAsset = null;

							session.Logger.DebugFormat("Downloading non object task inventory item {0}", taskItem.Name);
							InventoryAsset = Commands.Downloaders.Downloader.Download(session, be.Inventory.Id, UUID.Zero, taskItem);
							if(InventoryAsset != null && InventoryAsset.Data != null)
							{
								string filename = InventoryAsset.AssetId.ToString() + ArchiveConstants.ASSET_TYPE_TO_EXTENSION[(sbyte)InventoryAsset.AssetType];
								
								OpenMetaverse.Assets.AssetWearable wearable = null;
								if(InventoryAsset.AssetType == AssetType.Clothing)
									wearable = new OpenMetaverse.Assets.AssetClothing(new UUID(InventoryAsset.AssetId), InventoryAsset.Data);
								else if(InventoryAsset.AssetType == AssetType.Bodypart)
									wearable = new OpenMetaverse.Assets.AssetBodypart(new UUID(InventoryAsset.AssetId), InventoryAsset.Data);
		
								// wearable items can contain texture references
								// make sure we get those textures as well
								if(wearable != null)
									foreach(UUID textureID in wearable.Textures.Values)
										DownloadAndSaveTexture(session, textureID);
								
								_archive.AddAssetFile(ArchiveConstants.ASSETS_PATH + filename, InventoryAsset.Data);
							} else {
								session.Logger.ErrorFormat("Failed to download '{0}' of type {1}", taskItem.Name, taskItem.AssetType.ToString());
							}
						}
					}
				}
			}
			return taskInventoryItemIsProtected;
		}
		#endregion Save Task inventory
		
		#region helper functions
		public static UUID GrabUuidFromFilename(string name)
		{
			Regex uuidPattern = new Regex("[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}", RegexOptions.Compiled);
			
			
			Match result = uuidPattern.Match(name);
			if(result.Success)
				return new UUID(result.Value);
			else
				return UUID.Zero;
		}

		public static AssetType GrabAssetTypeFromFilename(string name)
		{
			foreach(KeyValuePair<string, sbyte> kv in ArchiveConstants.EXTENSION_TO_ASSET_TYPE)
			{
				if(name.EndsWith(kv.Key))
				   return (AssetType)kv.Value;
			}
			
			return AssetType.Unknown;
		}
		
		public void AddInventoryNode(string name, UUID itemID, UUID assetID, InventoryType invType, AssetType assetType)
		{
			byte[] inventoryData = BuildInventoryElement(name, itemID, assetID, invType, assetType);
			_archive.AddInventoryFile(ArchiveConstants.INVENTORY_PATH + name + ".xml", inventoryData);
		}
		
		private byte[] BuildInventoryElement(string name, UUID itemID, UUID assetID, InventoryType invType, AssetType assetType)
		{
			using( MemoryStream memory = new MemoryStream())
			{
                using (StreamWriter stream = new StreamWriter(memory))
                {
                    XmlTextWriter writer = new XmlTextWriter(stream);
					
					writer.WriteStartElement("InventoryItem");
					writer.WriteElementString("Name", name);
					writer.WriteElementString("ID", itemID.ToString());
					writer.WriteElementString("InvType", ((int)invType).ToString());
					writer.WriteElementString("CreatorUUID", "ospa:n=Snowcrash Short");
					writer.WriteElementString("CreationDate", "1282518830");
					writer.WriteElementString("Owner", "83918e9c-72e6-4ebd-9385-ae9a468d9c3c");
					writer.WriteElementString("Description", "(No Description)");
					writer.WriteElementString("AssetType", ((int)assetType).ToString());
					writer.WriteElementString("AssetID", assetID.ToString());
					writer.WriteElementString("SaleType", "0");
					writer.WriteElementString("SalePrice", "0");
					writer.WriteElementString("BasePermissions", "2147483647");
					writer.WriteElementString("CurrentPermissions", "2147483647");
					writer.WriteElementString("EveryOnePermissions", "0");
					writer.WriteElementString("NextPermissions", "2147483647");
					writer.WriteElementString("Flags", "0");
					writer.WriteElementString("GroupID", UUID.Zero.ToString());
					writer.WriteElementString("GroupOwned", "False");
					writer.WriteEndElement();
                    writer.Flush();
                }
            	return memory.ToArray();
			}
		}

		#endregion helper functions
	}
}
